#include "hexview.h"
#include "qmath.h"
#include <QPainter>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QDebug>
#include <QHBoxLayout>

HexView::HexView(QWidget *parent) : QAbstractScrollArea(parent)
{
    QHBoxLayout *layout = new QHBoxLayout(this);
    this->offsetView = new HexOffsetView(this);
    layout->addWidget(offsetView);
    layout->addSpacing(20);
    this->byteView = new HexByteView(this);
    byteView->setCursor(Qt::IBeamCursor);
    layout->addWidget(byteView);
    layout->addSpacing(20);
    this->stringView = new HexStringView(this);
    stringView->setCursor(Qt::IBeamCursor);
    layout->addWidget(stringView);
    layout->addStretch(100);
    this->setLayout(layout);

    this->font = QFont("Courier New", 11);
    this->headerRowPadding = 10;
    this->columnPadding = 10;
    this->bytesPerLine = 16;
    this->padding = 8;
    this->lineOffset = 0;

    QFontMetrics fm(font);
    this->charSize = QPoint(fm.width('A'), fm.height());

    connect(byteView,SIGNAL(bytesSelected(uint, uint)),this,SLOT(selectBytes(uint, uint)));
    connect(stringView,SIGNAL(bytesSelected(uint, uint)),this,SLOT(selectBytes(uint, uint)));
}

HexView::~HexView()
{
    //No need to clear children view, as parent destroys children automatically
    //disconnect done by object destructor too
}

//just clears the background with standard color
void HexView::paintEvent(QPaintEvent *e)
{
    QPainter painter;
    painter.begin(viewport());
    painter.setBrush(Qt::white);
    painter.drawRect(viewport()->rect());
    painter.end();
}

//override as somehow our wheel events are consumed and not propagated
bool HexView::event(QEvent *e)
{
    switch(e->type())
    {
        case QEvent::Wheel:
            wheelEvent(static_cast<QWheelEvent *>(e));
            break;
        default:
            break;
    }
    return QAbstractScrollArea::event(e);
}

void HexView::wheelEvent(QWheelEvent *event)
{
    int numDegrees = event->delta() / 8;
    this->verticalScrollBar()->setValue(verticalScrollBar()->value() - numDegrees);
}

void HexView::copyBytesToClipboard()
{
    QClipboard *cb = QApplication::clipboard();
    cb->setText(this->byteView->getSelectedText());
}

/*! \brief Loads numBytes Bytes from a specified buffer and handles their display on the views
  *
  * \param buffer A pointer to the buffer, containing the bytes to be displayed
  * \param numBytes The number of bytes to be displayed from the buffer
  */
void HexView::loadBytes(uchar *buffer, uint numBytes)
{
    //calculate total lines
    uint totalLines = qCeil(numBytes / this->bytesPerLine);
    //include header spacings
    uint lineHeight = this->charSize.y() + this->padding;
    uint headerLines = qCeil(this->getHeaderHeight() / lineHeight);

    //we adjust scrollbar....we count in lines
    this->verticalScrollBar()->setSingleStep(1);
    this->verticalScrollBar()->setPageStep(this->getVisibleLines() - headerLines - 1);
    this->verticalScrollBar()->setMinimum(0);
    this->verticalScrollBar()->setMaximum(totalLines + headerLines - this->verticalScrollBar()->pageStep());

    offsetView->setBufferLength(numBytes);
    byteView->loadBytes(buffer, numBytes);
    stringView->loadBytes(buffer, numBytes);
}

uint HexView::getVisibleLines()
{
    return qCeil(height() / (charSize.y() + padding));
}


//Scrolling down has negative Y values
void HexView::scrollContentsBy(int lineDiffX, int lineDiffY)
{

    this->lineOffset = lineOffset - lineDiffY;
    this->viewport()->update();
}

/*!
  * \brief Selects the selected bytes on all views
  */
void HexView::selectBytes(uint startByte, uint endByte)
{
    byteView->setSelectedBytes(startByte, endByte);
    stringView->setSelectedBytes(startByte, endByte);
    byteView->update();
    stringView->update();
}

HexDataView::HexDataView(HexView *parent) : QWidget(parent)
{
    this->parent = parent;
    this->selectedStartByteIndex = 0;
    this->selectedEndByteIndex = 0;
}

//default implementation
QPoint HexDataView::convertPixelToCharCoord(QPoint &pixelPoint)
{
    uint discreteCoordX = pixelPoint.x() /  getCellWidth();
    //allow a minimal coordinate of 0, as it may get negative if we are outside of the dataView area
    uint discreteCoordY = 0;
    if(pixelPoint.y() > parent->getHeaderHeight())
    {
        discreteCoordY = (pixelPoint.y() - parent->getHeaderHeight()) / getCellHeight();
    }
    return QPoint(discreteCoordX,discreteCoordY);
}

QPoint HexDataView::convertCharCoordToPixel(QPoint &charCoord)
{
    uint pixelX = charCoord.x() * this->getCellWidth();
    uint pixelY = parent->getHeaderHeight() + charCoord.y() * this->getCellHeight();
    return QPoint(pixelX,pixelY);
}

uint HexDataView::getCellWidth() const
{
    return parent->getCharSize().x() + parent->getPadding();
}

uint HexDataView::getCellHeight() const
{
    return parent->getCharSize().y() + parent->getPadding();
}

void HexDataView::loadBytes(uchar *buffer, uint numBytes)
{
    this->buffer = buffer;
    this->bufferLength = numBytes;
}

void HexDataView::mousePressEvent(QMouseEvent *event)
{
    if(event->button() == Qt::LeftButton)
    {
        QPoint point(qMax(0, event->x()),qMax(0, event->y()));
        QPoint discretizedPoint = this->convertPixelToCharCoord(point);
        uint index = (parent->getLineOffset() + discretizedPoint.y()) * parent->getBytesPerLine() + discretizedPoint.x();
        this->selectedStartByteIndex = index;
        this->selectedEndByteIndex = index;
    }
}

void HexDataView::mouseMoveEvent(QMouseEvent *event)
{
    //mouse tracking off, so we only get mousemove when mouse down
    QPoint point(qMax(0, event->x()),qMax(0, event->y()));
    QPoint discretizedPoint = this->convertPixelToCharCoord(point);
    uint newIndex = (parent->getLineOffset() + discretizedPoint.y()) * parent->getBytesPerLine() + discretizedPoint.x();
    //only send signal if new position
    if(newIndex != this->selectedEndByteIndex)
    {
        this->selectedEndByteIndex = newIndex;
        emit this->bytesSelected(selectedStartByteIndex, selectedEndByteIndex);
    }
}

void HexDataView::mouseReleaseEvent(QMouseEvent *event)
{
    emit this->bytesSelected(selectedStartByteIndex, selectedEndByteIndex);
}

void HexDataView::paintEvent(QPaintEvent *e)
{
    QPainter painter;
    painter.begin(this);
    painter.setPen(Qt::blue);
    painter.setFont(parent->getFont());

    uint bytesPerLine = parent->getBytesPerLine();
    uint padding = parent->getPadding();
    uint lineHeight = this->getCellHeight();
    uint numVisibleLines = parent->getVisibleLines();

    uint startLine = this->parent->getLineOffset();

    if(this->hasSelection())
    {
        //paint selected
        painter.setPen(Qt::yellow);
        painter.setBrush(Qt::yellow);
        uint startByte, endByte;
        //may be reversed marked
        if(selectedStartByteIndex < selectedEndByteIndex)
        {
            startByte = selectedStartByteIndex;
            endByte = selectedEndByteIndex;
        }
        else
        {
            startByte = selectedEndByteIndex;
            endByte = selectedStartByteIndex;
        }
        uint length = endByte - startByte;
        //position index in first selected line
        uint startXIndex = startByte % bytesPerLine;
        uint leftXPixel = startXIndex * this->getCellWidth();
        uint startYIndex = (startByte - (startLine * bytesPerLine)) / bytesPerLine;
        //position index of last byte of last selected line
        uint endYIndex = endByte % bytesPerLine;
        uint topYPixel = parent->getHeaderHeight() + padding + startYIndex * lineHeight;
        uint numMarkedBytesOnFirstLine = 0;
        if(startXIndex + length > bytesPerLine)
        {
            //Multiple lines, draw full lines first, then last line
            numMarkedBytesOnFirstLine = bytesPerLine - startXIndex;
            uint numAdditionalFullSelectedLines = (length - numMarkedBytesOnFirstLine) / bytesPerLine;
            if(numAdditionalFullSelectedLines > 0)
            {
               painter.drawRect(QRect(0, topYPixel + lineHeight, bytesPerLine * this->getCellWidth() - padding, lineHeight * numAdditionalFullSelectedLines));
            }
            //last line drawn
            if(endYIndex != 0)
            {
                painter.drawRect(QRect(0, topYPixel + (numAdditionalFullSelectedLines + 1) * lineHeight, endYIndex * this->getCellWidth() - padding, lineHeight));
            }
        }
        else
        {
            numMarkedBytesOnFirstLine = endByte % bytesPerLine - startXIndex;
        }
        //first line drawn
        painter.drawRect(QRect(leftXPixel, topYPixel, numMarkedBytesOnFirstLine * this->getCellWidth() - padding, lineHeight));
    }

    //draw bytes
    painter.setPen(Qt::blue);
    painter.setBrush(Qt::NoBrush);
    for(uint i = 0; i < numVisibleLines; i++)
    {
        for(uint j = 0; j < bytesPerLine; j++)
        {
            uint index = (startLine + i) * bytesPerLine + j;
            if(index >= this->bufferLength) break;
            //might get read over the buffer -> what to do?
            QString hexByteString = this->displayCellContent(this->buffer[index]);
            painter.drawText(j * this->getCellWidth(), parent->getHeaderHeight() + (i+1) * lineHeight, hexByteString );
        }
    }

    painter.end();
}

QString HexDataView::getSelectedText()
{
    if(this->hasSelection())
    {
        int size = qAbs(this->selectedEndByteIndex - this->selectedStartByteIndex);
        if(size > 2048) size = 2048;
        //convert to byteArray without copying data
        QByteArray array = QByteArray((char*)&this->buffer[this->selectedStartByteIndex], size);
        return QString(array);
    }
    return QString();
}

//HexOffsetView
HexOffsetView::HexOffsetView(HexView *parent) : QWidget(parent)
{
    this->parent = parent;
    this->numAddressBits = 32;
    this->bufferLength = 0;

    this->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
}

QSize HexOffsetView::sizeHint() const
{
    int width = this->numAddressBits / 4 * parent->getCharSize().x();
    int height = 0;
    return QSize(width, height);
}

void HexOffsetView::paintEvent(QPaintEvent *event)
{
    QPainter painter;
    painter.begin(this);
    painter.setPen(Qt::blue);
    painter.setFont(parent->getFont());
    uint charHeight = parent->getCharSize().y();
    uint padding = parent->getPadding();
    uint lineHeight = charHeight + padding;
    uint bytesPerLine = parent->getBytesPerLine();
    painter.drawText(0, charHeight, tr("Offset"));
    uint numVisibleLines = parent->getVisibleLines();

    uint startLine = this->parent->getLineOffset();
    uint offset = startLine * bytesPerLine;

    for(uint i = 1; i <= numVisibleLines; i++)
    {
        if(offset >= this->bufferLength) break;
        painter.drawText(0, parent->getHeaderHeight() + i * lineHeight, QString("%1").arg(offset, numAddressBits / 4, 16, QChar('0')));
        offset += bytesPerLine;
    }
    painter.end();
}

//HexByteView
HexByteView::HexByteView(HexView *parent) : HexDataView(parent)
{
    this->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
}

uint HexByteView::getCellWidth() const
{
    return (2 * parent->getCharSize().x()) + parent->getPadding();
}

uint HexByteView::getCellHeight() const
{
    return parent->getCharSize().y() + parent->getPadding();
}

QSize HexByteView::sizeHint() const
{
    int width = parent->getBytesPerLine() * this->getCellWidth();
    int height = 0;
    return QSize(width, height);
}

QString HexByteView::displayCellContent(uchar byte)
{
    return QString("%1").arg(byte, 2, 16, QChar('0'));
}

void HexByteView::paintEvent(QPaintEvent *e)
{
    QPainter painter;
    painter.begin(this);
    painter.setPen(Qt::blue);
    painter.setFont(parent->getFont());

    //draw offset header
    uint bytesPerLine = parent->getBytesPerLine();
    for(uint offset = 0; offset < bytesPerLine; offset++)
    {
        painter.drawText(offset * this->getCellWidth(), parent->getCharSize().y() , QString("%1").arg(offset, 2, 16, QChar('0')));
    }

    painter.end();

    HexDataView::paintEvent(e);
}

//HexStringView
HexStringView::HexStringView(HexView *parent) : HexDataView(parent)
{
    this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
}

QSize HexStringView::sizeHint() const
{
    int width = parent->getBytesPerLine() * this->getCellWidth();
    int height = 0;
    return QSize(width, height);
}

QString HexStringView::displayCellContent(uchar byte)
{
    //only printable ASCII chars
    if((byte > 0x1F) && (byte < 0x7F))
    {
        return QChar(byte);
    }
    return QChar('.');
}
